查看原文
其他

进程和线程

L0x1c 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:L0x1c



看了张银奎老师的书,记录一下。有很多比较官方的概念我直接抄了下来,所以不要喷我抄书,爱所有反卷局的大宝贝们!


进程和线程

处于运行状态的程序又称为进程,每个进程运行在自己的空间中,空间相对独立,受操作系统保护,在每个进程空间中,一般都会有一个或者多个线程在运行。

1


进程资源


在操作系统的规则中,资源一般都是针对于进程来分配的,必须要先有一个进程,才能对其分配资源。
 
在Windows操作系统中,每个进程都拥有如下的资源:

1、一个虚拟的地址空间,又被称为进程空间

2、全局唯一的进程ID,简称PID ( Client ID )

3、一个可执行映像,也是该进程的可执行文件在内存中的表示

4、一个或者多个线程

5、一个位于内核空间中的名为EPROCESS的数据结构,用来记录该进程的关键信息,包括进程的创建时间,映像文件名称等等

6、一个位于内核空间中的对象句柄表,用来记录和索引该进程所创建/打开的内核对象,操作系统根据该表格将用户模式下的句柄翻译为指向内核对象的指针

7、一个用于描述内存目录表起始位置的基地址,简称页目录基地址 ( DirBase ),当CPU切换到该任务/进程的时候,会将该地址加载到页表基地址寄存器CR3,这样当前进程的虚拟地址才会被翻译为正确的物理地址

8、一个位于用户空间中的进程环境块PEB

9、一个访问令牌,用于表示该进程的用户,安全组以及优先级


 
!process 0 0 执行该命令可以列出系统内所有的进程,第一个参数用来指定要显示的进程ID ( 因为叫Client ID 所以这里就是Cid ),0代表了所有进程,第二个参数用来指定要显示的进程属性,0代表只显示最基本的进程属性。
可以在命令后加上程序文件的名字来过滤,这样命令执行后,只显示wermgr进程的属性。
  • 第一行代表了EPROCESS结构的地址
  • 下面的三行代表了进程的关键的属性


进程的SessionId代表了该进程所在的Windows会话 ( session ) 的ID号, 当有多个用户同时登陆的时候,Windows会给每个登录用户建立一个会话,每个会话都有自己的WorkStation和Desktop,这样大家就可以在不同的会话中共用一个Windows系统,对于XP来说,只有一个用户登录的时候,用户启动的程序和系统服务都运行在session 0,当切换到另一个用户账号的时候,系统会建立Session 1,以此类推。
测试了一下,确实奥!如果我们net usr add后进行Switch User的时候我们的SessionId会进行++, 从Windows Vista开始,只允许系统服务运行在Session 0,系统启动后便会自动创建,当用户登录的时候会创建另一个会话 一般叫做 Session 1,所以用户登录到系统的时候一般会看至少两个Csrss在运行,系统启动早期创建的几个特殊进程不属于任何会话,所以他们的SessionId为空,比如system Registr 等等。

Cid为进程ID,进程ID是表示进程的一个整数,很多用户态的函数用它作为标识进程的参数,在内核空间的代码中,主要使用EPROCESS指针来标识进程。
Parent Cid是父进程的进程ID,既创建该进程的那个进程的进程ID。

DirBase 代表了该进程顶级页表的位置,也就是CPU切换到该进程的时候,CR3寄存器的内容,也就是当时看火哥的那个讲的说页表目录CR3寄存器内容指向的就是第一个表的位置,该寄存器是将虚拟地址转换成物理地址必须要的参数数值,顶级的页表叫页目录表,所以这个字段的名字叫做页目录基地址,DirBase字段的位定义与当前使用的分页模式有关,在32位的分页模式的时候,DirBase低12位总是0,高20位是该进程的页目录的页帧编号 ( PFN ),比如DirBase是上面的0xA2C83000,那么PFN为0xA2C83,那么就会有一个小疑问PFN是什么?这里我们就先把他当作一个index,是一个数组,等到时候变强了我再来看!

在IA32 分页模式下,CR3低12位会因为CR4的PCIDE位(17位)而不同,PCIDE( Process-Context Identifiers Enable )。
当PCIDE开启为1的时候,CPU会缓存多个进程的页表信息,低12位变成进程上下文的ID号,为了防止CPU熔断和幽灵的漏洞。NT内核中引入了KVA影子的安全补丁,这个补丁会使用CPU的PCID功能,但是并不知道CPU Meltdown和Spectry是啥!得去查查。

CPU Meltdown和Spectry漏洞:


回来啦!这两个漏洞比较类似,大概的意思就是:我问二木哥:我昨天啊~看见了你和一个女生在一起逛街,你们是不是?虽然我昨天压根儿什么都没看到,但是我说完,他要是愣了一会(愣神比较久),回了一句:我凭什么要告诉你?虽然LSP没有承认,但是显然我已经知道了答案。那么这次的漏洞也是同理,利用了CPU的两个特殊的机制:其中Meltdown利用了CPU的乱序执行,Spectre利用了CPU的预测执行。
 
什么是CPU乱序执行和预测执行呢?

预测执行:在整个OS系统中,假设某个恶意软件去问操作系统:刚才那小伙!登录的密码第一个数字是1吗?操作系统回复:不关你的事,你没有权限知道。虽然这个过程本身是没有问题的,但是问题出在:操作系统答复了,但是他心里想了一遍这个答案,尤其是当对方问到一个正确的数字时,他回答的稍微犹豫了几毫秒,这个事情被恶意软件注意到了,就可以通过反复的提问,最后猜到了密码是什么。

乱序执行:假设二木哥 有a b c三个老婆,其中a老婆在娘家没有探访的权限,但是b和c在自己家里随时可以访问,此时执行下面这个条件表达式!x= a?b:c 由于a老婆不能去,如果二木哥去了!就会被揍!但实际上,二木哥会在检查a是否可访问的同时,预先就往下执行了,等到结果回来,已经根据a的结果完成了b或者c访问,反正不能亏!只是还没有赋给x而已,那么经过加载的b或者c会放缓存里。虽然报错了,但如果再次访问就会比较快,于是再次访问b和c,根据返回的时间快慢,就可以猜到a的内容!假设我想知道a有没有买化妆品,如果执行b成功了,就可以知道a确实买了化妆品。

知道了上面的瞎扯淡原理后,我们来好好看看是为啥!

Meltdown
; rcx = kernel address; rbx = probe_arraymov al, byte [rcx]shl rax, 0xcmov rbx, qword [rbx + rax]

rcx存放了用户层不可以访问的内核地址,rbx存放了探测的数组。
 
当我们要访问rcx的时候,就会触发异常,该指令及之后的指令对寄存器的修改都会被丢弃,处理器重新回到能正常执行的指令中,但由于处理器采用乱序执行方式,在等待处理器完成该指令执行的同时(权限检查结束之前),后面两条指令已经被执行了(尽管最终会被丢弃)。
 
将指令3读取到的数据乘以4096(4KB)也就是一页,因为对可访问的内存进行了访问后会将内存页放入到缓存中去,根据这个速度就可以进行判定,因为缓存的速度会很快。
那么作为攻击者去对缓存进行测信道攻击就会知道哪个内存页访问过了,从而推断出被访问的内核内存数据。

NT内核中也引入了叫做KVA的补丁,会开启PCID的功能,查看一下KVA的模式为1,cr4的寄存器位17为1。

ObjectTable的含义为该进程的内核对象和句柄表格,Windows使用该表格将句柄翻译成内核对象的指针。
HandleCount表示ObjectTable所含的表项的数量。



2


进程空间


介绍了32位的进程空间,64位的,就还是内核空间是共享的,但是要为系统中的所有进程服务,所以不允许被某个进程任意访问和破坏,所以内核空间的特权级别高于用户空间,又介绍了64位的,又说了怎么看EPROCESS结构 dt _EPROCESS xxxxxxxx 用 !process EPROCESS的结构地址 来显示进程的关键信息,还介绍了PEB是进程环境块,里面有进程大多数用户模式的信息,.process xxxxxxx 设置当前进程后 dt _PEB xxxxxxx。
 

访问模式


重点来了!
 
Windows定义了两种访问模式 ( access mode )
  • 用户模式(user mode, 用户态)
  • 内核模式( Kernel mode, 内核态)

应用程序的代码运行在用户模式之下,操作系统的代码运行在内核模式之下,对于x86处理器来说并没有任何寄存器来表明我们当前处于何种模式,优先级只是代码或者数据所在的内存段或者页的一个属性。
 
虽然不可以直接访问R0层的,但是用户程序可以通过调用系统服务来间接的访问内核空中的数据或间接调用执行内核空间代码,当我们调用系统服务的时候,会从用户模式切换到内核模式,调用结束的时候返回用户模式,这就是模式切换。
 
那么在线程KTHREAD结构中,定义了UserTime和KernelTime的两个字段,用来记录线程在用户模式和内核模式运行的时间(时钟中断次数为单位),模式切换通过软中断或者专门的fast system call指令实现。


使用INT 2E切换到内核模式:

我们可以很简明的看到我们该调用首先呢转到Kernel32的ReadFile函数,ReadFile函数对参数进行简单的检查后调用Ntdll中的NtReadFile函数。
 
kernel32:
 
Ntdll:
 
我是个好奇的人,所以直接去看了win2k3的源码把这个KiSystemService给逆了。
_KiSystemService proc ENTER_SYSCALL kss_a, kss_t ; set up trap frame and save state ?FpoValue = 0 ;; (eax) = Service number; (edx) = Callers stack pointer; (esi) = Current thread address;; All other registers have been saved and are free.;; Check if the service number within valid range; _KiSystemServiceRepeat: mov edi, eax ; copy system service number ;edi = eax 等于了系统调用号 shr edi, SERVICE_TABLE_SHIFT ; isolate service table number and edi, SERVICE_TABLE_MASK ; ; 这里用来确定是去找系统服务表还是驱动的表 mov ecx, edi ; save service table number add edi, [esi]+ThServiceTable ; compute service descriptor address ; 指向系统服务表 mov ebx, eax ; save system service number ; ebx位系统调用号 and eax, SERVICE_NUMBER_MASK ; isolate service table offset ; eax为系统服务下标;; If the specified system service number is not within range, then attempt; to convert the thread to a GUI thread and retry the service dispatch.; cmp eax, [edi]+SdLimit ; check if valid service jae Kss_ErrorHandler ; if ae, try to convert to GUI thread ; 检查系统调用的下标是否超过了表的大小 ;; If the service is a GUI service and the GDI user batch queue is not empty,; then call the appropriate service to flush the user batch.; cmp ecx, SERVICE_TABLE_TEST ; test if GUI service ; 跳转GUI API服务 jne short Kss40 ; if ne, not GUI service mov ecx, PCR[PcTeb] ; get current thread TEB address ; ecx指向KPCR xor ebx, ebx ; get number of batched GDI calls KiSystemServiceAccessTeb: or ebx, [ecx]+TbGdiBatchCount ; may cause an inpage exception jz short Kss40 ; if z, no batched calls push edx ; save address of user arguments push eax ; save service number call [_KeGdiFlushUserBatch] ; flush GDI user batch pop eax ; restore service number pop edx ; restore address of user arguments ;; The arguments are passed on the stack. Therefore they always need to get; copied since additional space has been allocated on the stack for the; machine state frame. Note that we don't check for the zero argument case -; copy is always done regardless of the number of arguments because the; zero argument case is very rare.; Kss40: inc dword ptr PCR[PcPrcbData+PbSystemCalls] ; system calls ; 直接跳转到这里进行++系统调用数 FPOFRAME ?FpoValue, 0 mov esi, edx ; (esi)->User arguments ; 3环的指针 mov ebx, [edi]+SdNumber ; get argument table address ; 系统服务表 xor ecx, ecx mov cl, byte ptr [ebx+eax] ; (ecx) = argument size mov edi, [edi]+SdBase ; get service table address mov ebx, [edi+eax*4] ; (ebx)-> service routine ; 指向的函数 sub esp, ecx ; allocate space for arguments shr ecx, 2 ; (ecx) = number of argument DWORDs mov edi, esp ; (es:edi)->location to receive 1st arg cmp esi, _MmUserProbeAddress ; check if user address jae kss80 ; if ae, then not user address KiSystemServiceCopyArguments: rep movsd ; copy the arguments to top of stack. ; Since we usually copy more than 3 ; arguments. rep movsd is faster than ; mov instructions. ; 复制参数 ;; Make actual call to system service;kssdoit: CAPSTARTX <_KiSystemService,ebx> call ebx ; call system service ; call 系统服务 CAPENDX <_KiSystemService> kss60:

两张表一张是找内核函数,另一个是驱动函数,那么我们看的eax 里存一个值,系统调用号或者服务号,这个东西的低12位就是函数参数表和函数地址表的下标,而第13位如果是0,表示找系统服务表,如果是1,那么找驱动函数的的表。
 
总结来说就是KiSystemService会根据服务ID从SSDT中找调用的服务地址和参数描述,然后将参数从用户态复制到线程的内核态,然后调用真正的函数执行操作,操作结束后返回给KiSystemService,该函数会把结果返回到用户态,在进行系统调用的时候,CPU需要从内存中加载门描述符和段描述符才可以得到KiSystemServer的地址,然后进行权限检查,检查源代码位置和目标代码位置的代码段的权限。


快速系统调用


系统调用是非常繁琐的事情,那么肯定想要减少这些开销的啦!
  • 系统调用服务例程的地址放到寄存器中来避免读IDT这样的内存操作(??因为IDT 是INT xxx IDT表的东西),因为读寄存器的速度比读内存的速度要快很多
  • 避免权限检查,也就是使用特殊的指令让CPU省去对系统服务调用来说根本不需要的权限检查

奔腾 II 处理器引入的SYSENTER/SYSEXIT 指令正是按照这一思路设计的,AMD K7 引入的 SYSCALL/SYSRETURN指令 都是因为这个速度的目的来设计的,对比INT 2E来说,使用这些指令可以加快系统调用的速度,因此利用这些指令来进行系统调用的方式叫做快速系统调用。
 
因为Windows 2000 或者之前的Windows的操作系统不支持快速系统调用,他们只能使用INT 2E的方式来进行系统调用,这时候我们需要去小路哥哥那里白嫖一个windows XP回来!
 
 
这让人兴奋的界面,直接来了把扫雷。

稳了!继续学习!
 
在Windows XP和Windows Server 2003或更高的版本在启动的过程中会通过CPUID 指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位),EDX的11位代表了SEP标志位,来代表是否支持快速系统调用,如果系统不支持就还会使用INT 2E的方式。
 

进行快速系统调用需要做的准备:


在GDT表中建立4个段描述符,用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段,在GDT中他们的排列方式应该严格按照上述的方式进行排列。

设置专门用于系统调用的MSR寄存器,SYSENTER_EIP_MSR用于指定新的程序指针,是SYSENTER指令要跳转的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为该例程是Windows内核中专门用于受理快速系统调用的,SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段,SYSENTER_ESP_MSR用于指定真的栈指针(ESP)。
| MSR Name | MSR Add | Useful |
| ---------------- | ------- | -------------------- |
| SYSENTER_CS_MSR | 174 | 目标代码段的CS选择子 |
| SYSENTER_ESP_MSR | 175 | 目标ESP |
| SYSENTER_EIP_MSR | 176 | 目标EIP |
该地址用于Kernel debug时,通过rdmsr/wrmsr指令来读/写这3个寄存器。
174的位置我们应该不陌生了,因为0环的时候切换寄存器看到的cs经常是08,那么我们看一下EIP的位置:
可以看到是nt!KiFastCallEntry的地址。

会将一小段名为SystemCallStub的代码复制到SharedUserData的内存区,内存区会被映射到每个Win32的进程空间中,这样每次进行系统调用的时候,NTDll.DLL中的stub函数就会调用这段SystemCallstub代码,该代码根据系统硬件的不同而不同,对于IA-32就会使用SYSENTER,对于AMD就会使用SYSCALL指令。

打开windbg开始,看一下ntdll中的ReadFile函数!这里碰到了点坑,就是符号老加载不上,如果想去看ntdll的东西,需要切换一下进程。
可以很清楚的看到 SharedUserData!SystemCallstub 的地址给了edx寄存器,Call的是edx中的地址。
将当前的esp的寄存器放入到edx寄存器当中 ( 个人认为这里应该是要去获取r3层的参数所以到时候可以通过edx寄存器来取参数,因为esp需要修改 ),因为sysenter要调用KiFastCallEntry,所以去看看!
8053e540 b923000000 mov ecx,23h8053e545 6a30 push 30h8053e547 0fa1 pop fs8053e549 8ed9 mov ds,cx8053e54b 8ec1 mov es,cx
在 KPCR(Processor Cotnrol Region)区域的 +0x40 位置是 TSS 指针(指向一个 KTSS 结构),KPCR 结构的地址在0xffdff000:
0x80042000 _KTSS为TSS的指针,所以KTSS的结构为:
0级的Esp值,这指向一个 KTRAP_FRAME 结构 V86Es 成员。
8053e54d 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h]8053e553 8b6104 mov esp,dword ptr [ecx+4]8053e556 6a23 push 23h

这个!ffdff040就是KPCR TSS指针所指向的KTSS的指针,所以他将这个值给了ecx,再通过ecx+4获得ESP0的地址,因为当前的esp所在的是KTRAP_FRAME的V86Es,所以push 23修改了V86Es上层成员HardwareSegSs。

KTRAP_FRAME 结构


在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里,因为我们已经获取到了ESP0的地址,KTRAP_FRAME的基址为 Esp0 - 0x7c。

在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里,在前面我们看到 KTRAP_FRAME 结构的地址被赋予 esp 寄存器,因此:KTRAP_FRAME 结构就是 KiFastCallEntry() 函数的 stack 区域。

开始逆!

kd> u nt!Kifastcallentry L20nt!KiFastCallEntry:8053e540 b923000000 mov ecx,23h 8053e545 6a30 push 30h8053e547 0fa1 pop fs ;fs = 0x308053e549 8ed9 mov ds,cx ;ds = 0x238053e54b 8ec1 mov es,cx ;es = 0x23 都是段选择子有关的修改相应的权限的8053e54d 8b0d40f0dfff mov ecx,dword ptr ds:[0FFDFF040h] ;ecx = _KTSS ptr8053e553 8b6104 mov esp,dword ptr [ecx+4] ;esp = ESP0 -> KTRAP_FRAME的0x7c位置的V86Es8053e556 6a23 push 23h ;_KTRAP_FRAME.HardwareSegSs = 0x238053e558 52 push edx ;_KTRAP_FRAME.HardwareEsp = edx (edx为三环的esp)8053e559 9c pushfd ;_KTRAP_FRAME.EFlags = EFlags8053e55a 6a02 push 2 8053e55c 83c208 add edx,8 ;因为edx为三环的esp,所以这个步骤是要去取三环的参数8053e55f 9d popfd ;0环 EFlags = 0x28053e560 804c240102 or byte ptr [esp+1],2 ;EFlags的 IF位置18053e565 6a1b push 1Bh ;_KTRAP_FRAME.SegCs = 0x1B8053e567 ff350403dfff push dword ptr ds:[0FFDF0304h] ;_KTRAP_FRAME.Eip为 kd> u 7c92e4f4 ntdll!KiFastSystemCallRet8053e56d 6a00 push 0 ;_KTRAP_FRAME.ErrCode = 08053e56f 55 push ebp ;_KTRAP_FRAME.EBP = EBP8053e570 53 push ebx ;_KTRAP_FRAME.EBX = EBX8053e571 56 push esi ;_KTRAP_FRAME.ESI = ESI8053e572 57 push edi ;_KTRAP_FRAME.EDI = EDI8053e573 8b1d1cf0dfff mov ebx,dword ptr ds:[0FFDFF01Ch] ;ebx = _kpcr 0ffdff0008053e579 6a3b push 3Bh ;_KTRAP_FRAME.SegFs = 0x3B8053e57b 8bb324010000 mov esi,dword ptr [ebx+124h] ;esi = kpcr_kprcb_CurrentThread 8053e581 ff33 push dword ptr [ebx] ;_KTRAP_FRAME.ExceptionList = ExceptionList8053e583 c703ffffffff mov dword ptr [ebx],0FFFFFFFFh ;ExceptionList = -18053e589 8b6e18 mov ebp,dword ptr [esi+18h] ;ebp = kpcr_kprcb_CurrentThread_InitialStack8053e58c 6a01 push 1 ;_KTRAP_FRAME.PreviousPreviousMode = 1 代表三环过来的8053e58e 83ec48 sub esp,48h ;esp = ntdll!_KTRAP_FRAME_ptr8053e591 81ed9c020000 sub ebp,29Ch ;8053e597 c6864001000001 mov byte ptr [esi+140h],1 ;kpcr_kprcb_CurrentThread_PreviousMode = 1 表示从3环调用来8053e59e 3bec cmp ebp,esp ;比较当前的esp和ebp的位置8053e5a0 759a jne nt!KiFastCallEntry2+0x47 (8053e53c) ;如果不相同则异常,正常都在_KTRAP_FRAME_ptr8053e5a2 83652c00 and dword ptr [ebp+2Ch],0 ;_KTRAP_FRAME.Dr7 = 08053e5a6 f6462cff test byte ptr [esi+2Ch],0FFh ;DebugActive8053e5aa 89ae34010000 mov dword ptr [esi+134h],ebp ;TrapFrame = ebp(_KTRAP_FRAME_ptr)8053e5b0 0f854afeffff jne nt!Dr_FastCallDrSave (8053e400) ;检测是否被调试的状态8053e5b6 8b5d60 mov ebx,dword ptr [ebp+60h] ;ebx = _KTRAP_FRAME.Ebp8053e5b9 8b7d68 mov edi,dword ptr [ebp+68h] ;edi = _KTRAP_FRAME.Eip8053e5bc 89550c mov dword ptr [ebp+0Ch],edx ;_KTRAP_FRAME.DbgArgPointer = edx 保存3环的参数指针8053e5bf c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h ;_KTRAP_FRAME.DbgArgMark = 0BADB0D00h8053e5c6 895d00 mov dword ptr [ebp],ebx ;_KTRAP_FRAME.DbgEbp = _KTRAP_FRAME.Ebp8053e5c9 897d04 mov dword ptr [ebp+4],edi ;_KTRAP_FRAME.DbgEip = _KTRAP_FRAME.Eip8053e5cc fb sti
 
结构THRED的E0偏移为ServiceTable
 
KeServiceDescriptorTableShadow
 
8053e5cd 8bf8 mov edi,eax ;系统服务例程号8053e5cf c1ef08 shr edi,8 ;右移八位8053e5d2 83e730 and edi,30h ; index8053e5d5 8bcf mov ecx,edi ; ecx = index8053e5d7 03bee0000000 add edi,dword ptr [esi+0E0h] ;esi+0E0h = ServiceTable_ptr 代表是去找index=1还是=0的结构的项8053e5dd 8bd8 mov ebx,eax ;系统服务例程号8053e5df 25ff0f0000 and eax,0FFFh ;取后24位8053e5e4 3b4708 cmp eax,dword ptr [edi+8] ;edi + 8的位置为MaxServiceNumber:最大的系统服务例程号8053e5e7 0f8345fdffff jae nt!KiBBTUnexpectedRange (8053e332)8053e5ed 83f910 cmp ecx,10h ;比较index是否为18053e5f0 751a jne nt!KiFastCallEntry+0xcc (8053e60c)8053e5f2 8b0d18f0dfff mov ecx,dword ptr ds:[0FFDFF018h]8053e5f8 33db xor ebx,ebx ;ebx = 08053e5fa 0b99700f0000 or ebx,dword ptr [ecx+0F70h] ;调试的时候发现这里都是08053e600 740a je nt!KiFastCallEntry+0xcc (8053e60c)8053e602 52 push edx8053e603 50 push eax8053e604 ff15e43f5580 call dword ptr [nt!KeGdiFlushUserBatch (80553fe4)]8053e60a 58 pop eax8053e60b 5a pop edx8053e60c ff0538f6dfff inc dword ptr ds:[0FFDFF638h] ;系统调用的次数+18053e612 8bf2 mov esi,edx ;三环的edx给了当前的esi8053e614 8b5f0c mov ebx,dword ptr [edi+0Ch] ;ebx指向了参数表的8053e617 33c9 xor ecx,ecx ;ecx = 08053e619 8a0c18 mov cl,byte ptr [eax+ebx] ;读取参数的个数ArgumentSizeTable:提供每个例程所需要的参数大小,这个值将要用来从 caller 里复制多少个参数8053e61c 8b3f mov edi,dword ptr [edi] ;获取ServiceRoutineTable(提供真正的系统服务例程的地址)的地址8053e61e 8b1c87 mov ebx,dword ptr [edi+eax*4] ;读取服务例程地址8053e621 2be1 sub esp,ecx ;在当前栈上开辟空间容纳参数8053e623 c1e902 shr ecx,2 ;ecx/4 获取参数个数8053e626 8bfc mov edi,esp ;edi指向栈8053e628 3b35d4995580 cmp esi,dword ptr [nt!MmUserProbeAddress (805599d4)] ;看是否属于用户空间8053e62e 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (8053e7dc)8053e634 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] ;复制参数到当前栈上8053e636 ffd3 call ebx ;调用最终的服务例程
 

表示在windbg里面跟了一边 清晰多了!但是windbg太不好看了!决定换回windbg preview?

 

画一手流程图:

 

ecx用来指定SYSEXIT返回用户模式时的目标地址,当使用INT 2E进行系统调用的时候,由于INT N的指令会i东将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会让栈中保存的cs和EIP返回合适的位置,但是因为SYSENTER指令不会向栈中压入要返回的位置,所以sysexit指令需要通过其他机制知道需要返回的位置。


书上给了一个例子,来加深这个系统调用的一个理解,因为栈上记录了函数相互调用时的参数和返回地址等信息,栈回溯是从栈上去找到这些信息,然后显示出来的过程。

可以看到第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是执行的位置。

逆向调用


之前我们知道了用户模式进入内核模式的两种方式,通过这两种形式,用户模式的代码可以调用位于内核模式的系统服务,如果我们想让内核模式的代码主动调用用户模式代码的时候,那么这种调用叫做逆向调用。
 
逆向调用的过程:
 
内核代码使用KiCallUserMode发起调用,接下来的执行的过程和系统返回的KiSystemCallExit类似,只是进入用户模式的时候执行的是NTDLL.dll中的KiUserCallbackDispatcher,而后该函数会调用内核希望调用的用户态函数,用户模式的工作完成后,执行返回动作的函数会执行INT 2B的指令,从而触发0x2B的异常。
可以知道对应的异常的函数是nt!KiCallbackReturn。

实例分析


如果要切换.process 一定要加上/i /p 这样cr3才会换回来,上下文才会过来。
 
 
可以看到,栈回溯是 nt!KeUserModeCallback -> KiCallUserMode -> KiUserCallbackDispatcher。
 
 
可以看到整个过程有之前说到的nt!KiFastCallEntry,通过服务号去找对应的函数,执行完后将消息组织好去调用KeUserModeCallback函数,从而进行逆向调用,用户模式状态下的过程没有体现出来,但是可以看到XyCallbackReturn函数用于返回内核模式,调用int 2B nt!KiCallbackReturn 返回到内核模式。
 


3


线程


线程是进程中的生命,一个进程内有一个或者多个线程,进程创建初期或者进程退出和销毁的过程中,进程内可能没有任何线程。

ETHREAD


NT内核使用ETHREAD结构来描述线程,在内核代码中,大多时候使用ETHREAD结构的地址来索引线程,执行.thread命令,就可以显示出当前线程的ETHREAD结构地址。
查看一下KTHREAD的结构:

包含了线程的各种的属性,ETHREAD开头的512字节是一个KTHREAD结构,也称作为线程控制块( TCB ),里面主要是供内核调度线程时使用的。
 
 
Header字段为Dispatcher_Header类型,是NT内核的线程调度器的别名,代表了分发CPU时间片的意思。
 
因为字段太多了,所以调试的时候谜一般不直接观察,使用扩展命令!thread,从而用这个扩展命令来更好的显示线程属性。

右上角的RUNNING on processor 0代表了这个线程正运行在0号CPU上,来源于KTHREAD结构的state字段。

为枚举类型KTHREAD_STATE,共有9个值从而代表了线程的状态。


可以看到当前的线程,在Running。
NT内核给每个CPU定义了一个名为处理控制块的PRCB的庞大结构,这个结构中有一个DispatcherReadyListHead的数组,包括了32个元素,代表了线程的32个优先级,每个元素是一个LIST_ENTRY的结构,从而起到链表头的作用,用来挂接对应优先级的就绪线程。
各个线程状态之间的切换关系:
 
上面所说的那个链表的事情找到了答案,我用的是xp的系统32位的,那时候的这个结构在TEB中的WaitListEntry中。
 
 
如果线程处于等待状态下,那么!thread命令会显示出等待的原因,在KTHREAD结构中有一个叫做WaitReason的字段。 

从而记录线程等待的原因,长度为1字节,是枚举的类型,名为KWAIT_REASON,在公开的PDB符号文件中,包含了这个枚举类型的定义。


TEB


NT内核定义了线程环境快TEB来描述线程的用户空间的信息,包括了用户态栈,异常处理,错误码,线程局部存储等等。
 
windbg调试应用程序的时候,可以使用 !teb 来显示当前线程的teb 结构的位置,我用的是本机win10 挂载的 notepad。

NT内核会使用CPU的硬件机制来快速定位当前线程的TEB,所以内核在创建线程的时候,会分配专门的内存也来用作TEB,将地址记录在KTHREAD中,所以TEB的地址总是按照页对齐的。
 
同时也可以用dt _TEB的命令查看TEB的信息。



4


WOW进程


现在大多数的windows系统是64位的,运行在支持64位CPU的地方,但是为了兼容32位的应用程序,64位windows系统可以运行32位的应用程序,这样运行在64位内核上的32位进程有一个专门的名字,叫做WoW64(Windows 32 on Windows 64)进程。

架构:

 
由于32位的代码是不能与64位内核交互的,所以中间转换层为了解决这个问题设计,转接层本身为64位的模块,他给32位的应用程序营造了一个32位的环境,主要负责指针长度的转换和解决API兼容等问题。


工作过程


可以使用.effmach命令再两种代码间进行切换。
 
.effmach amd64切换到64位的模式下,
可以看到上面的wow64和wow64win都是转接层的核心的模块。

切换到32位下,可以看到再WoW的进程中,有两个ntdll的模块,一个是64位的,一个是32位的,WINDBG会在后加载进程的32位版本的模块名加上基地址。
 
在WoW的进程中,有很多的东西是双份的,每个进程有两个PEB,每个线程有两个TEB,有两个栈。
 
WINDBG的wow64exts扩展模块专门是为调试WoW进程从而设计的,info命令可以显示WoW进程的双份的资产。

Guest代表了32位的代码 Native指向的为64位的代码。


执行过程

可以看到需要call Wow64SystemServiceCall。
jmp Wow64Transition (KiFastSystemCall)
段选择子33 指向的是64位的段,这个就是从32位兼容模式到64位模式的方法。
 
我去查看了0x33段选择子 base是0 所以直接跳转到了0x773A7009的位置,我这里做了一个测试拿win7做的。
 
现在去逆以下wow64cpu!CpupReturnFromSimulatedCode 这里我个人推荐!直接去调notepad可以一点点看这个过程。
 
之前的cs的状态是23的状态,因为经过跳转的过程变成33。

可以看到Long进行了改变,interl手册的段描述符中的高21位置是L位代表了是32位还是64位。

以看到gdtr表中他们的不同。

可以看到当前的esp装的返回地址,那么+4放下推的话就是call之前的参数
00000000`754f271e 67448b0424 mov r8d,dword ptr [esp] ;取出32位的返回地址放到r8d因为到时候需要返回的时候需要
00000000`754f2723 458985bc000000 mov dword ptr [r13+0BCh],r8d ;可以看到r13是64位环境的栈底,保存了32位的返回地址00000000`754f272a 4189a5c8000000 mov dword ptr [r13+0C8h],esp ;保存了32位的时候的栈帧
但是这里有个疑问!0x1480是个什么,直接去看一下TEB的结构!
发现叫做TlsSlots,但是TlsSlots是什么东东!待我去学习一下!
 
http://www.nynaeve.net/?p=181
 
是个线程槽了,这里存储了64位线程的一些关键环境以及32位线程的关键数据。
00000000`754f2731 498ba42480140000 mov rsp,qword ptr [r12+1480h] ;r12为teb的地址,这里取出了64位的rsp00000000`754f2739 4983a4248014000000 and   qword ptr [r12+1480h],0         ;清空缓存
 
edx存放的是参数的地址:
00000000`754f2742 448bda mov r11d,edx ;r11d存放的是参数的地址

可以看到r15是一个表rcx相当于跳转的服务号。
00000000`754f2745 41ff24cf        jmp     qword ptr [r15+rcx*8]        ;跳转

跳转后的环境:

syscall会跳到:

可以看出来WoW进程中的32版本的NTDLL.DLL 在执行系统调用的时候,会调用特殊的WoW64SystemServiceCall函数,切换到64位的WoW转换层。


注册表重定向


64位的windows操作系统会对WoW进程的注册表访问实施重定向,如果程序中访问的路径为HKEY_LOCAL_MACHINE\software,那么就会被重定向到HKEY_LOCAL_MACHINE\software\Wow6432Node,所以i使用注册表编辑器的时候,如果要查看WoW进程的设置,那么也要去查看Wow6432Node表键下的,有一部分的表键是供两类程序共享的,有一类则是重定向的。
 
test代码:
#include <windows.h>#include <stdio.h> int main(){ //找到系统的启动项 char* Register = "Software\\Microsoft\\Windows\\CurrentVersion\\Run"; char* Myapp = "C:\\Users\\Administrator\\Desktop\\ctfmon.exe"; HKEY hKey; //打开注册表启动项 if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, Register, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS) { RegSetValueExA(hKey, "Mytest", 0, REG_SZ, (BYTE*)Myapp, strlen(Myapp)); //关闭注册表 RegCloseKey(hKey); printf("succeed!\n"); } else { printf("Failed!"); return -1; } return 0;}


注册表反射


考虑到COM组件,有32位的也有64位的,为了让用户再一个版本中所做的设置在另一个版本中也有效,所以windows实现了一种叫做注册表反射的机制,对于某些COM组件有关的表键,来自一边的修改会自动更新到另一边。



5


创建进程


我们打开一个新的程序,windows会使用一套标准的流程来创建一个新进程:

1、在父进程的用户空间中打开要执行的映像文件,确定其名称,类型和系统对它的设置选项

2、进入父进程的内核空间,为新进程创建EPROCESS结构,进程地址空间,KPROCESS结构和PEB

3、创建初始线程,但是创建的时候指定了挂起标志,并不会立刻开始运行

4、通知子系统服务程序,对于windows程序,通知windows子系统服务进程CSRSS

5、初始线程开始在内核空间执行

6、通过APC的方式,在新进程自己的空间中执行初始化动作,主要是通过NTDLL.DLL中的加载器,加载进程所依赖的DLL文件


Windows Internals上面详细的介绍了!但是我去翻了没太看懂,有点蒙。 到时候写Windows Internals笔记的时候 会好好研究!写出来的东西让更多人明白一下!



6


最小进程和Pico进程


对于正常的NT进程,NT内核会自动创建一些设施并且将他们映射到进程的用户空间当中,如PEB,TEB等等,因为考虑到NTDLL.DLL的特殊,NT内核也会自动将NTDLL.DLL映射到普通进程用户模式空间中,对于某些特殊的情况,这些的动作可能是多余而带有副作用的,所以设计了最小进程和Pico进程。
 
这里,最小进程和Pico进程推荐自己看书奥!这里没有好的dump文件比较难搞,日后有时间补上。



 


看雪ID:L0x1c

https://bbs.pediy.com/user-home-873515.htm

*本文由看雪论坛 L0x1c 原创,转载请注明来自看雪社区



官网:https://www.bagevent.com/event/6334937



# 往期推荐

1.All About Crypto - CTF竞赛密码学方向指南

2. ghostscript 命令注入漏洞分析

3.16位实模式切换32位保护模式过程详解

4. 高Glibc版本下的堆骚操作解析

5.新人PWN堆Heap总结off-by-null专场

6. CVE-2012-3569 VMware OVF Tool格式化字符串漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存